using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Net;
using System.IO;

namespace GathererImport
{
    class GathererImport
    {
        // See the respective build methods (defined in the constructor) to see how
        // these structures are used.
        private Dictionary<string, int> mineNodeNames = new Dictionary<string,int>();
        private Dictionary<string, string> zoneCodes = new Dictionary<string, string>();
        private IList<IList<string>> zoneIDsByContinent = new List<IList<string>>();

        // A simple dictionary linking the zone ID (as defined in Gatherer) to a
        // plain old Zone object
        private Dictionary<string, Zone> zones = new Dictionary<string, Zone>();

        // These are defined in Gatherer.  Since they are versioned, hopefully they would
        // be automatically upgraded in subsequent Gatherer versions.
        private int itemsDBVersion = 2;
        private int dropRatesDBVersion = 1;

        /// <summary>
        /// Main constructor which initializes our skeleton data
        /// </summary>
        public GathererImport()
        {
            buildZoneCodes();
            buildZoneIDsByContinent();
            buildMineNodeNames();
        }

        /// <summary>
        /// The method that does all of the real work, collecting all necessary data
        /// and sending it out to a file.
        /// </summary>
        /// <param name="fileName">The filename for output</param>
        public void process(string fileName)
        {
            foreach (KeyValuePair<string, int> mineNodeIDs in mineNodeNames)
            {
                Console.Out.WriteLine("Collecting all instances of " + mineNodeIDs.Key + "...");
                storeObjectLocations(mineNodeIDs.Value);
            }

            Console.Out.WriteLine("Writing contents to file...");
            FileStream fs = File.Create(fileName);
            StreamWriter sw = new StreamWriter(fs);

            writeGatherItems(sw);
            sw.WriteLine("GatherDrops = nil");
            writeDropRates(sw);

            sw.Close();
            fs.Close();
        }

        // Writes to the stream the LUA data for the GatherItems structure
        private void writeGatherItems(StreamWriter sw)
        {
            sw.WriteLine("GatherItems = {");

            IEnumerator<IList<string>> continentEnum = zoneIDsByContinent.GetEnumerator();
            int continentCount = 0;
            while (continentEnum.MoveNext())
            {
                continentCount++;
                IList<string> zoneList = continentEnum.Current;
                sw.WriteLine("\t{");
                IEnumerator<string> zoneEnum = zoneList.GetEnumerator();
                while (zoneEnum.MoveNext())
                {
                    string zoneID = zoneEnum.Current;
                    Zone zone = getZoneByZoneID(zoneID);
                    if (zone.ItemIDs.Count == 0)
                        continue;

                    sw.WriteLine("\t\t[\"" + zoneID + "\"] = {");
                    IEnumerator<int> zoneIDEnum = zone.ItemIDs.GetEnumerator();
                    while (zoneIDEnum.MoveNext())
                    {
                        int itemID = zoneIDEnum.Current;
                        sw.WriteLine("\t\t\t[" + itemID + "] = {");
                        IEnumerator<Coordinate> coordEnum = zone.GetCoordinates(itemID).GetEnumerator();
                        int coordCount = 0;
                        while (coordEnum.MoveNext())
                        {
                            coordCount++;
                            Coordinate c = coordEnum.Current;
                            sw.WriteLine("\t\t\t\t{");
                            sw.WriteLine("\t\t\t\t\t" + (c.X / 100) + ", -- [1]");
                            sw.WriteLine("\t\t\t\t\t" + (c.Y / 100) + ", -- [2]");
                            sw.WriteLine("\t\t\t\t\t1, -- [3]");
                            sw.WriteLine("\t\t\t\t\t1174771529, -- [4]");
                            sw.WriteLine("\t\t\t\t\t1174771529, -- [5]");
                            sw.WriteLine("\t\t\t\t}, -- [" + coordCount + "]");
                        }
                        sw.WriteLine("\t\t\t\t[\"gtype\"] = \"MINE\",");
                        sw.WriteLine("\t\t\t},");
                    }
                    sw.WriteLine("\t\t},");
                }
                sw.WriteLine("\t}, -- [" + continentCount + "]");
            }
            sw.WriteLine("\t[\"dbVersion\"] = " + itemsDBVersion + ",");
            sw.WriteLine("}");
        }

        // Writes to the stream all LUA data for the Gatherer_DropRates structure
        private void writeDropRates(StreamWriter sw)
        {
            sw.WriteLine("Gatherer_DropRates = {");

            IEnumerator<IList<string>> continentEnum = zoneIDsByContinent.GetEnumerator();
            int continentCount = 0;
            while (continentEnum.MoveNext())
            {
                continentCount++;
                sw.WriteLine("\t{");
                IList<string> zoneList = continentEnum.Current;
                IEnumerator<string> zoneEnum = zoneList.GetEnumerator();
                while (zoneEnum.MoveNext())
                {
                    string zoneID = zoneEnum.Current;
                    Zone zone = getZoneByZoneID(zoneID);
                    if (zone.ItemIDs.Count == 0)
                        continue;

                    sw.WriteLine("\t\t[\"" + zoneID + "\"] = {");
                    IEnumerator<int> itemIDEnum = zone.ItemIDs.GetEnumerator();
                    while (itemIDEnum.MoveNext())
                    {
                        int itemID = itemIDEnum.Current;
                        sw.WriteLine("\t\t\t[" + itemID + "] = {");
                        sw.WriteLine("\t\t\t\t[\"total\"] = 0,");
                        sw.WriteLine("\t\t\t},");
                    }
                    sw.WriteLine("\t\t},");
                }
                sw.WriteLine("\t}, -- [" + continentCount + "]");
            }
            sw.WriteLine("\t[\"dbVersion\"] = " + dropRatesDBVersion + ",");
            sw.WriteLine("}");
        }

        // Builds all mining node names.  It is keyed by the mining node description
        // (which really serves no technical purpose, it is just what gets displayed
        // to the user when running this program), and the values are the blizzard 
        // object IDs for each of these objects.  It is used in Gatherer's LUA structures
        // and WoWHead conveniently uses it in their URL when fetching the coordinate data.
        private void buildMineNodeNames()
        {
            mineNodeNames.Add("Small Thorium Vein", 324);
            mineNodeNames.Add("Incendicite Mineral Vein", 1610);
            mineNodeNames.Add("Copper Vein", 1731);
            mineNodeNames.Add("Tin Vein", 1732);
            mineNodeNames.Add("Silver Vein", 1733);
            mineNodeNames.Add("Gold Vein", 1734);
            mineNodeNames.Add("Iron Deposit", 1735);
            mineNodeNames.Add("Mithril Deposit", 2040);
            mineNodeNames.Add("Truesilver Deposit", 2047);
            mineNodeNames.Add("Lesser Bloodstone Deposit", 2653);
            mineNodeNames.Add("Indurium Mineral Vein", 19903);
            mineNodeNames.Add("Ooze Covered Silver Vein", 73940);
            mineNodeNames.Add("Ooze Covered Gold Vein", 73941);
            mineNodeNames.Add("Ooze Covered Truesilver Deposit", 123309);
            mineNodeNames.Add("Ooze Covered Mithril Deposit", 123310);
            mineNodeNames.Add("Ooze Covered Thorium Vein", 123848);
            mineNodeNames.Add("Dark Iron Deposit", 165658);
            mineNodeNames.Add("Rich Thorium Vein", 175404);
            mineNodeNames.Add("Ooze Covered Rich Thorium Vein", 177388);
            mineNodeNames.Add("Hakkari Thorium Vein", 180215);
            mineNodeNames.Add("Fel Iron Deposit", 181555);
            mineNodeNames.Add("Adamantite Deposit", 181556);
            mineNodeNames.Add("Khorium Vein", 181557);
            mineNodeNames.Add("Rich Adamantite Deposit", 181569);
        }

        // Builds all zone codes.  The keys are the literal zone names as parsed from
        // the WowHead HTML page.  So spelling and the seemingly unnecessary single quote
        // escapes are actually very important.  If a WowHead zone location is parsed and
        // not found in the zoneCodes dictionary, it will shoot out an error and die
        // with an unhandled exception.  It works as of 3/25/07 but if further zones are
        // opened in the game, this will probably need manual updating.  The values of
        // this dictionary are the zone codes that Gatherer uses in it's LUA configuration
        // structure.
        private void buildZoneCodes()
        {
            zoneCodes.Add("Ashenvale", "ASHENVALE");
            zoneCodes.Add("Azshara", "AZSHARA");
            zoneCodes.Add("Azuremyst Isle", "AZUREMYST_ISLE");
            zoneCodes.Add("The Barrens", "BARRENS");
            zoneCodes.Add("Bloodmyst Isle", "BLOODMYST_ISLE");
            zoneCodes.Add("Darkshore", "DARKSHORE");
            zoneCodes.Add("Darnassis", "DARNASSUS");
            zoneCodes.Add("Desolace", "DESOLACE");
            zoneCodes.Add("Durotar", "DUROTAR");
            zoneCodes.Add("Dustwallow Marsh", "DUSTWALLOW_MARSH");
            zoneCodes.Add("Felwood", "FELWOOD");
            zoneCodes.Add("Feralas", "FERALAS");
            zoneCodes.Add("Moonglade", "MOONGLADE");
            zoneCodes.Add("Mulgore", "MULGORE");
            zoneCodes.Add("Ogrimmar", "ORGRIMMAR");
            zoneCodes.Add("Silithus", "SILITHUS");
            zoneCodes.Add("Stonetalon Mountains", "STONETALON_MOUNTAINS");
            zoneCodes.Add("Tanaris", "TANARIS");
            zoneCodes.Add("Teldrassil", "TELDRASSIL");
            zoneCodes.Add("The Exodar", "EXODAR");
            zoneCodes.Add("Thousand Needles", "THOUSAND_NEEDLES");
            zoneCodes.Add("Thunder Bluff", "THUNDER_BLUFF");
            zoneCodes.Add("Un\\'Goro Crater", "UNGORO_CRATER");
            zoneCodes.Add("Winterspring", "WINTERSPRING");
            zoneCodes.Add("Alterac Mountains", "ALTERAC_MOUNTAINS");
            zoneCodes.Add("Arathi Highlands", "ARATHI_HIGHLANDS");
            zoneCodes.Add("Badlands", "BADLANDS");
            zoneCodes.Add("Blasted Lands", "BLASTED_LANDS");
            zoneCodes.Add("Burning Steppes", "BURNING_STEPPES");
            zoneCodes.Add("Deadwind Pass", "DEADWIND_PASS");
            zoneCodes.Add("Dun Morogh", "DUN_MOROGH");
            zoneCodes.Add("Duskwood", "DUSKWOOD");
            zoneCodes.Add("Eastern Plaguelands", "EASTERN_PLAGUELANDS");
            zoneCodes.Add("Elwynn Forest", "ELWYNN_FOREST");
            zoneCodes.Add("Eversong Woods", "EVERSONG_WOODS");
            zoneCodes.Add("Ghostlands", "GHOSTLANDS");
            zoneCodes.Add("Hillsbrad Foothills", "HILLSBRAD_FOOTHILLS");
            zoneCodes.Add("The Hinterlands", "HINTERLANDS");
            zoneCodes.Add("Ironforge", "IRONFORGE");
            zoneCodes.Add("Loch Modan", "LOCH_MODAN");
            zoneCodes.Add("Redridge Mountains", "REDRIDGE_MOUNTAINS");
            zoneCodes.Add("Searing Gorge", "SEARING_GORGE");
            zoneCodes.Add("Silvermoon City", "SILVERMOON");
            zoneCodes.Add("Silverpine Forest", "SILVERPINE_FOREST");
            zoneCodes.Add("Stormwind", "STORMWIND");
            zoneCodes.Add("Stranglethorn Vale", "STRANGLETHORN_VALE");
            zoneCodes.Add("Swamp of Sorrows", "SWAMP_OF_SORROWS");
            zoneCodes.Add("Tirisfal Glades", "TIRISFAL_GLADES");
            zoneCodes.Add("Undercity", "UNDERCITY");
            zoneCodes.Add("Western Plaguelands", "WESTERN_PLAGUELANDS");
            zoneCodes.Add("Westfall", "WESTFALL");
            zoneCodes.Add("Wetlands", "WETLANDS");
            zoneCodes.Add("Blade\\'s Edge Mountains", "BLADES_EDGE_MOUNTAINS");
            zoneCodes.Add("Hellfire Peninsula", "HELLFIRE_PENINSULA");
            zoneCodes.Add("Nagrand", "NAGRAND");
            zoneCodes.Add("Netherstorm", "NETHERSTORM");
            zoneCodes.Add("Shadowmoon Valley", "SHADOWMOON_VALLEY");
            zoneCodes.Add("Shattrath City", "SHATTRATH");
            zoneCodes.Add("Terokkar Forest", "TEROKKAR_FOREST");
            zoneCodes.Add("Zangarmarsh", "ZANGARMARSH");
        }

        // Builds a list of lists, grouping zone codes by continent.  This needed to be
        // done because Gatherer's LUA configuration has it's own list of lists grouping
        // zone information by continent.  This will probably need manual updating if
        // Blizzard opens more zones.
        private void buildZoneIDsByContinent()
        {
            zoneIDsByContinent.Clear();

            IList<string> kalimdor = new List<string>();
            kalimdor.Add("ASHENVALE");
            kalimdor.Add("AZSHARA");
            kalimdor.Add("AZUREMYST_ISLE");
            kalimdor.Add("BARRENS");
            kalimdor.Add("BLOODMYST_ISLE");
            kalimdor.Add("DARKSHORE");
            kalimdor.Add("DARNASSUS");
            kalimdor.Add("DESOLACE");
            kalimdor.Add("DUROTAR");
            kalimdor.Add("DUSTWALLOW_MARSH");
            kalimdor.Add("FELWOOD");
            kalimdor.Add("FERALAS");
            kalimdor.Add("MOONGLADE");
            kalimdor.Add("MULGORE");
            kalimdor.Add("ORGRIMMAR");
            kalimdor.Add("SILITHUS");
            kalimdor.Add("STONETALON_MOUNTAINS");
            kalimdor.Add("TANARIS");
            kalimdor.Add("TELDRASSIL");
            kalimdor.Add("EXODAR");
            kalimdor.Add("THOUSAND_NEEDLES");
            kalimdor.Add("THUNDER_BLUFF");
            kalimdor.Add("UNGORO_CRATER");
            kalimdor.Add("WINTERSPRING");

            IList<string> easternKingdoms = new List<string>();
            easternKingdoms.Add("ALTERAC_MOUNTAINS");
            easternKingdoms.Add("ARATHI_HIGHLANDS");
            easternKingdoms.Add("BADLANDS");
            easternKingdoms.Add("BLASTED_LANDS");
            easternKingdoms.Add("BURNING_STEPPES");
            easternKingdoms.Add("DEADWIND_PASS");
            easternKingdoms.Add("DUN_MOROGH");
            easternKingdoms.Add("DUSKWOOD");
            easternKingdoms.Add("EASTERN_PLAGUELANDS");
            easternKingdoms.Add("ELWYNN_FOREST");
            easternKingdoms.Add("EVERSONG_WOODS");
            easternKingdoms.Add("GHOSTLANDS");
            easternKingdoms.Add("HILLSBRAD_FOOTHILLS");
            easternKingdoms.Add("HINTERLANDS");
            easternKingdoms.Add("IRONFORGE");
            easternKingdoms.Add("LOCH_MODAN");
            easternKingdoms.Add("REDRIDGE_MOUNTAINS");
            easternKingdoms.Add("SEARING_GORGE");
            easternKingdoms.Add("SILVERMOON");
            easternKingdoms.Add("SILVERPINE_FOREST");
            easternKingdoms.Add("STORMWIND");
            easternKingdoms.Add("STRANGLETHORN_VALE");
            easternKingdoms.Add("SWAMP_OF_SORROWS");
            easternKingdoms.Add("TIRISFAL_GLADES");
            easternKingdoms.Add("UNDERCITY");
            easternKingdoms.Add("WESTERN_PLAGUELANDS");
            easternKingdoms.Add("WESTFALL");
            easternKingdoms.Add("WETLANDS");

            IList<string> outland = new List<string>();
            outland.Add("BLADES_EDGE_MOUNTAINS");
            outland.Add("HELLFIRE_PENINSULA");
            outland.Add("NAGRAND");
            outland.Add("NETHERSTORM");
            outland.Add("SHADOWMOON_VALLEY");
            outland.Add("SHATTRATH");
            outland.Add("TEROKKAR_FOREST");
            outland.Add("ZANGARMARSH");

            zoneIDsByContinent.Add(kalimdor);
            zoneIDsByContinent.Add(easternKingdoms);
            zoneIDsByContinent.Add(outland);
        }

        // This workhorse method pulls all of the relevent data from WoWHead's web pages,
        // parses them out, then stores them in the relevant data structures.  It's kind
        // of ugly, but then again so is text parsing in general.
        private void storeObjectLocations(int objectID)
        {
            string url = "http://www.wowhead.com/?object=" + objectID;
            WebClient webClient = new WebClient();
            string webContents = webClient.DownloadString(url);

            string[] lineTokens = webContents.Split(
                new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
            Regex lineTokenRegex = new Regex("This object can be found in");
            for (int i = 0; i < lineTokens.Length; i++)
            {
                Match match = lineTokenRegex.Match(lineTokens[i]);
                if (match.Success)
                {
                    string[] embedTokens = lineTokens[i].Split(
                        new string[] { "onclick" }, StringSplitOptions.RemoveEmptyEntries);
                    Regex embedTokenRegex = new Regex("mp_Embed\\('(.*?)',\\[(.*?)\\],this\\)");
                    for (int j = 0; j < embedTokens.Length; j++)
                    {
                        Match embedMatch = embedTokenRegex.Match(embedTokens[j]);
                        if (embedMatch.Success)
                        {
                            GroupCollection groups = embedMatch.Groups;
                            IList<Coordinate> coordinates = parseCoordinates(embedMatch.Groups[2].Value);
                            Zone zone = getZoneByDescription(embedMatch.Groups[1].Value);
                            for(int k=0;k<coordinates.Count;k++)
                            {
                                zone.AddItem(objectID, coordinates[k]);
                            }
                        }
                    }
                }
            }
        }

        // Convenience method to pull a Zone object based on a zone ID (aka zone code).
        // If a Zone doesn't exist for a specific key, the object will be created.
        private Zone getZoneByZoneID(string zoneID)
        {
            if (!zones.ContainsKey(zoneID))
                zones.Add(zoneID, new Zone());

            return zones[zoneID];
        }

        // Convenience method to pull a Zone object based on a zone description.
        // These descriptions are pulled straight from the WoWHead HTML parsing, so 
        // if a description is not found in our skeleton structures (defined in the build...()
        // methods) it will yell out the error and exit.
        private Zone getZoneByDescription(string zoneDescription)
        {
            if (!zoneCodes.ContainsKey(zoneDescription))
            {
                Console.Out.WriteLine("Zone " + zoneDescription + 
                    " does not show in our codes structure!");
                Environment.Exit(1);
            }

            string zoneCode = zoneCodes[zoneDescription];
            if (!zones.ContainsKey(zoneCode))
            {
                zones.Add(zoneCode, new Zone());
            }

            return zones[zoneCode];
        }

        // A method to parse the coordinate string from the WoWHead HTML data.  This is
        // just extracted out from the storeObjectLocations() method so it will be a 
        // tad more readable.
        private IList<Coordinate> parseCoordinates(String coordString)
        {
            IList<Coordinate> coordinates = new List<Coordinate>();
            string newCoordStr = coordString.Substring(1, coordString.Length - 2);
            string[] coordPairs = newCoordStr.Split(
                new string[] { "],[" }, StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < coordPairs.Length; i++)
            {
                string[] cTokens = coordPairs[i].Split(
                    new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                float xCoord = float.Parse(cTokens[0]);
                float yCoord = float.Parse(cTokens[1]);
                Coordinate c = new Coordinate(xCoord, yCoord);
                coordinates.Add(c);
            }

            return coordinates;
        }

        static void Main(string[] args)
        {
            if (args.Length < 1)
            {
                Console.Out.WriteLine("Usage: GathererImport <filename>");
                Environment.Exit(1);
            }

            GathererImport gi = new GathererImport();
            gi.process(args[0]);
        }
    }

    /// <summary>
    /// Simply stores the X and Y coordinates for a given node.
    /// </summary>
    class Coordinate
    {
        private float xCoordinate;
        public float X
        {
            get
            {
                return xCoordinate;
            }
        }

        private float yCoordinate;
        public float Y
        {
            get
            {
                return yCoordinate;
            }
        }

        public Coordinate(float xCoordinate, float yCoordinate)
        {
            this.xCoordinate = xCoordinate;
            this.yCoordinate = yCoordinate;
        }
    }

    /// <summary>
    /// A simple object that stores item information (item ID and coordinates) for
    /// each zone.  I don't even have a string identifier for it since it is categorized
    /// in one of the skeletal dictionaries (defined at the top of the GathererImport class).
    /// </summary>
    class Zone
    {
        private Dictionary<int, ICollection<Coordinate>> itemLocations = 
            new Dictionary<int,ICollection<Coordinate>>();

        public Zone() { }

        public void AddItem(int itemID, Coordinate coordinate)
        {
            if (!itemLocations.ContainsKey(itemID))
            {
                itemLocations.Add(itemID, new List<Coordinate>());
            }

            itemLocations[itemID].Add(coordinate);
        }

        public ICollection<int> ItemIDs
        {
            get
            {
                return itemLocations.Keys;
            }
        }

        public ICollection<Coordinate> GetCoordinates(int itemID)
        {
            return itemLocations[itemID];
        }
    }
}
